using UnityEngine;
using System.Collections;

[AddComponentMenu("Third Person Camera/Spring Follow Camera")]

public class SpringFollowCamera : MonoBehaviour
{
	public Transform target;
	public float distance = 4.0f;
	public float height = 1.0f;
	public float smoothLag = 0.2f;
	public float maxSpeed = 10.0f;
	public float snapLag = 0.3f;
	public float clampHeadPositionScreenSpace = 0.75f;
	public LayerMask lineOfSightMask = 0;
	
	private bool isSnapping = false;
	private Vector3 headOffset = Vector3.zero;
	private Vector3 centerOffset = Vector3.zero;
	private ThirdPersonController controller;
	private Vector3 velocity = Vector3.zero;
	private float targetHeight = 100000.0f;


	void Awake()
	{
		CharacterController characterController = (CharacterController)target.collider;
		if(characterController)
		{
			centerOffset = characterController.bounds.center - target.position;
			headOffset = centerOffset;
			headOffset.y = characterController.bounds.max.y - target.position.y;
		}
		if(target)
		{
			controller = target.GetComponent<ThirdPersonController>();
		}
		if(!controller)
		{
			Debug.Log("Assing a target to the camera with a Third person Controller");
		}
	}

	void LateUpdate()
	{
		Vector3 targetCenter = target.position + centerOffset;
		Vector3 targetHead = target.position + headOffset;
		
		if(controller.IsJumping())
		{
			//Only move the camera upwards if the target is very high
			float newTargetHeight = targetCenter.y + height;
			if((newTargetHeight < targetHeight) || (newTargetHeight - targetHeight >5))
			{
				targetHeight = targetCenter.y + height;
			}
		}
		
		//Always update target height while walking
		else
		{
			targetHeight = targetCenter.y + height;
		}
		
		//Snap when user presses Fire2
		if(Input.GetButton("Fire2") && !isSnapping)
		{
			velocity = Vector3.zero;
			isSnapping = true;
		}
		
		if(isSnapping)
		{
			ApplySnapping(targetCenter);
		}
		else
		{
			ApplyPositionDamping(new Vector3(targetCenter.x, targetHeight, targetCenter.z));
		}
		SetUpRotation(targetCenter, targetHead);
	}

	void ApplySnapping(Vector3 targetCenter)
	{
		Vector3 position = transform.position;
		Vector3 offset = position - targetCenter;
		
		offset.y = 0;
		float currentDistance = offset.magnitude;
		
		float targetAngle = target.eulerAngles.y;
		float currentAngle = transform.eulerAngles.y;
		
		currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle,ref velocity.x, snapLag);
		currentDistance = Mathf.SmoothDamp(currentDistance, distance,ref velocity.z, snapLag);
		
		Vector3 newPosition = targetCenter;
		newPosition += Quaternion.Euler(0, currentAngle, 0) * Vector3.back * currentDistance;
		
		newPosition.y = Mathf.SmoothDamp(position.y, targetCenter.y + height,ref velocity.y, smoothLag, maxSpeed);
		
		newPosition = AdjustLineOfSight(newPosition, targetCenter);
		
		transform.position = newPosition;
		
		//Stop snapping, close to target
		if(AngleDistance(currentAngle, targetAngle) < 3.0f)
		{
			isSnapping = false;
			velocity = Vector3.zero;
		}
	}

	public Vector3 AdjustLineOfSight(Vector3 newPosition, Vector3 target)
	{
		RaycastHit hit;
		if(Physics.Linecast(target, newPosition,out hit, lineOfSightMask.value))
		{
			velocity = Vector3.zero;
			return hit.point;
		}
		
		return newPosition;
	}

	void ApplyPositionDamping(Vector3 targetCenter)
	{
		//Attempt to maintain a constant distance on x-z plane
		//Y position is handled with separate sping
		Vector3 position = transform.position;
		Vector3 offset = position - targetCenter;
		offset.y = 0;
		Vector3 newTargetPos = offset.normalized * distance + targetCenter;
		
		Vector3 newPosition = Vector3.zero;
		
		newPosition.x = Mathf.SmoothDamp(position.x, newTargetPos.x, ref velocity.x, smoothLag, maxSpeed);
		//Debug.Log("newPosition.x" + newPosition.x);
		newPosition.z = Mathf.SmoothDamp(position.z, newTargetPos.z, ref velocity.z, smoothLag, maxSpeed);
		//Debug.Log("newPosition.z" + newPosition.z);
		newPosition.y = Mathf.SmoothDamp(position.y, targetCenter.y, ref velocity.y, smoothLag, maxSpeed);
			
		newPosition = AdjustLineOfSight(newPosition, targetCenter);
		
		transform.position = newPosition;		
	}

	void SetUpRotation(Vector3 centerPos, Vector3 headPos)
	{
		//Find the rotation around the y axis. 
		//When grounded, center camera
		//When jumping, keep the camera rotation, but rotate the camera if character out of view
		//When landing, smoothly interpolate towards centering character
		Vector3 cameraPos = transform.position;
		Vector3 offsetToCenter = centerPos-cameraPos;
		
		//Generate rotation around y axis
		Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
		
		Vector3 relativeOffset = Vector3.forward * distance + Vector3.down * height;
		transform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
		
		//Calculate the projected center position and top position
		Ray centerRay = camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1));
		Ray topRay = camera.ViewportPointToRay(new Vector3(0.5f, clampHeadPositionScreenSpace, 1));
		
		Vector3 centerRayPos = centerRay.GetPoint(distance);
		Vector3 topRayPos = topRay.GetPoint(distance);
		
		float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction);
		
		float heightToAngle = centerToTopAngle / (centerRayPos.y - topRayPos.y);
		
		float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y);
		if(extraLookAngle < centerToTopAngle)
		{
			extraLookAngle = 0;
		}
		else 
		{
			extraLookAngle = extraLookAngle - centerToTopAngle;
			transform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
		}
	}

	float AngleDistance(float a, float b)
	{
		a = Mathf.Repeat(a, 360f);
		b = Mathf.Repeat(b, 360f);
		
		return Mathf.Abs(b - a);
	}
}